/**
 * @module olcs.SynchronizedOverlay
 */
import olOverlay from 'ol/Overlay.js';
import { transform } from 'ol/proj.js';
import { removeNode, removeChildren } from './util.js';
import { unByKey as olObservableUnByKey } from 'ol/Observable.js';
/**
 * Options for SynchronizedOverlay
 * @typedef {Object} SynchronizedOverlayOptions
 * @property {!Cesium.Scene} scene
 * @property {olOverlay} parent
 * @property {!import('olsc/OverlaySynchronizer.js').default} synchronizer
 */
class SynchronizedOverlay extends olOverlay {
    /**
     * @param {olcsx.SynchronizedOverlayOptions} options SynchronizedOverlay Options.
     * @api
     */
    constructor(options) {
        const parent = options.parent;
        super(parent.getOptions());
        /**
         * @private
         * @type {?Function}
         */
        this.scenePostRenderListenerRemover_ = null;
        /**
         * @private
         * @type {!Cesium.Scene}
         */
        this.scene_ = options.scene;
        /**
         * @private
         * @type {!olcs.OverlaySynchronizer}
         */
        this.synchronizer_ = options.synchronizer;
        /**
         * @private
         * @type {!ol.Overlay}
         */
        this.parent_ = parent;
        /**
         * @private
         * @type {ol.Coordinate|undefined}
         */
        this.positionWGS84_ = undefined;
        /**
         * @private
         * @type {MutationObserver}
         */
        this.observer_ = new MutationObserver(this.handleElementChanged.bind(this));
        /**
         * @private
         * @type {Array.<MutationObserver>}
         */
        this.attributeObserver_ = [];
        /**
         * @private
         * @type {Array<ol.EventsKey>}
         */
        this.listenerKeys_ = [];
        // synchronize our Overlay with the parent Overlay
        const setPropertyFromEvent = event => this.setPropertyFromEvent_(event);
        this.listenerKeys_.push(this.parent_.on('change:position', setPropertyFromEvent));
        this.listenerKeys_.push(this.parent_.on('change:element', setPropertyFromEvent));
        this.listenerKeys_.push(this.parent_.on('change:offset', setPropertyFromEvent));
        this.listenerKeys_.push(this.parent_.on('change:position', setPropertyFromEvent));
        this.listenerKeys_.push(this.parent_.on('change:positioning', setPropertyFromEvent));
        this.setProperties(this.parent_.getProperties());
        this.handleMapChanged();
        this.handleElementChanged();
    }
    /**
     * @param {Node} target
     * @private
     */
    observeTarget_(target) {
        if (!this.observer_) {
            // not ready, skip the event (this occurs on construction)
            return;
        }
        this.observer_.disconnect();
        this.observer_.observe(target, {
            attributes: false,
            childList: true,
            characterData: true,
            subtree: true
        });
        this.attributeObserver_.forEach((observer) => {
            observer.disconnect();
        });
        this.attributeObserver_.length = 0;
        for (let i = 0; i < target.childNodes.length; i++) {
            const node = target.childNodes[i];
            if (node.nodeType === 1) {
                const observer = new MutationObserver(this.handleElementChanged.bind(this));
                observer.observe(node, {
                    attributes: true,
                    subtree: true
                });
                this.attributeObserver_.push(observer);
            }
        }
    }
    /**
     *
     * @param {ol.Object.Event} event
     * @private
     */
    setPropertyFromEvent_(event) {
        if (event.target && event.key) {
            this.set(event.key, event.target.get(event.key));
        }
    }
    /**
     * Get the scene associated with this overlay.
     * @see ol.Overlay.prototype.getMap
     * @return {!Cesium.Scene} The scene that the overlay is part of.
     * @api
     */
    getScene() {
        return this.scene_;
    }
    /**
     * @override
     */
    handleMapChanged() {
        if (this.scenePostRenderListenerRemover_) {
            this.scenePostRenderListenerRemover_();
            removeNode(this.element);
        }
        this.scenePostRenderListenerRemover_ = null;
        const scene = this.getScene();
        if (scene) {
            this.scenePostRenderListenerRemover_ = scene.postRender.addEventListener(this.updatePixelPosition.bind(this));
            this.updatePixelPosition();
            const container = this.stopEvent ?
                this.synchronizer_.getOverlayContainerStopEvent() : this.synchronizer_.getOverlayContainer();
            if (this.insertFirst) {
                container.insertBefore(this.element, container.childNodes[0] || null);
            }
            else {
                container.appendChild(this.element);
            }
        }
    }
    /**
     * @override
     */
    handlePositionChanged() {
        // transform position to WGS84
        const position = this.getPosition();
        if (position) {
            const sourceProjection = this.parent_.getMap().getView().getProjection();
            this.positionWGS84_ = transform(position, sourceProjection, 'EPSG:4326');
        }
        else {
            this.positionWGS84_ = undefined;
        }
        this.updatePixelPosition();
    }
    /**
     * @override
     */
    handleElementChanged() {
        function cloneNode(node, parent) {
            const clone = node.cloneNode();
            if (node.nodeName === 'CANVAS') {
                const ctx = clone.getContext('2d');
                ctx.drawImage(node, 0, 0);
            }
            if (parent) {
                parent.appendChild(clone);
            }
            if (node.nodeType != Node.TEXT_NODE) {
                clone.addEventListener('click', (event) => {
                    node.dispatchEvent(new MouseEvent('click', event));
                    event.stopPropagation();
                });
            }
            const nodes = node.childNodes;
            for (let i = 0; i < nodes.length; i++) {
                if (!nodes[i]) {
                    continue;
                }
                cloneNode(nodes[i], clone);
            }
            return clone;
        }
        removeChildren(this.element);
        const element = this.getElement();
        if (element) {
            if (element.parentNode && element.parentNode.childNodes) {
                for (const node of element.parentNode.childNodes) {
                    const clonedNode = cloneNode(node, null);
                    this.element.appendChild(clonedNode);
                }
            }
        }
        if (element.parentNode) {
            // set new Observer
            this.observeTarget_(element.parentNode);
        }
    }
    /**
     * @override
     */
    updatePixelPosition() {
        const position = this.positionWGS84_;
        if (!this.scene_ || !position) {
            this.setVisible(false);
            return;
        }
        let height = 0;
        if (position.length === 2) {
            const globeHeight = this.scene_.globe.getHeight(Cesium.Cartographic.fromDegrees(position[0], position[1]));
            if (globeHeight && this.scene_.globe.tilesLoaded) {
                position[2] = globeHeight;
            }
            if (globeHeight) {
                height = globeHeight;
            }
        }
        else {
            height = position[2];
        }
        const cartesian = Cesium.Cartesian3.fromDegrees(position[0], position[1], height);
        const camera = this.scene_.camera;
        const ellipsoidBoundingSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(), 6356752);
        const occluder = new Cesium.Occluder(ellipsoidBoundingSphere, camera.position);
        // check if overlay position is behind the horizon
        if (!occluder.isPointVisible(cartesian)) {
            this.setVisible(false);
            return;
        }
        const cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
        // check if overlay position is visible from the camera
        if (cullingVolume.computeVisibility(new Cesium.BoundingSphere(cartesian)) !== 1) {
            this.setVisible(false);
            return;
        }
        this.setVisible(true);
        const pixelCartesian = this.scene_.cartesianToCanvasCoordinates(cartesian);
        const pixel = [pixelCartesian.x, pixelCartesian.y];
        const mapSize = [this.scene_.canvas.width, this.scene_.canvas.height];
        this.updateRenderedPosition(pixel, mapSize);
    }
    /**
     * Destroys the overlay, removing all its listeners and elements
     * @api
     */
    destroy() {
        if (this.scenePostRenderListenerRemover_) {
            this.scenePostRenderListenerRemover_();
        }
        if (this.observer_) {
            this.observer_.disconnect();
        }
        olObservableUnByKey(this.listenerKeys_);
        this.listenerKeys_.splice(0);
        if (this.element.removeNode) {
            this.element.removeNode(true);
        }
        else {
            this.element.remove();
        }
        this.element = null;
    }
}
export default SynchronizedOverlay;
